package com.anji.hyperneat.nd;

import java.util.Iterator;

import com.anji.hyperneat.nd.NDFloatArray.MatrixIterator;

/**
 * Provides support for N-Dimensional int array.
 * Underlying data structure is a 1D array, for fastest possible access. 
 * Only provides functionality for storing and retrieving values from the array;
 * doesn't provide support for any matrix type calculations. 
 * For speed reasons, gets/sets do not perform bounds checking and do not check that
 * you've specified a coordinate for each dimension.  
 * @author Shaun Lusk
 *
 */
public class NDIntArray implements Cloneable {
	// The data
	private int m[];
	
	// Contains the size of each dimension; dimension.length = # of dimensions
	private final int[] dimensions;
	
	/* The pre-calculated "area" for counting by a dimension as it maps to a single dimensional array;
	 * e.g., for an XY space with D0=X and D1=Y, asize[0] = 1 (counting by x's)  and asize[1] = MaxVal(X).
	 */
	private final int[] asize;
	
	/**
	 * Creates an N-Dimensional int matrix with the specified dimensions.
	 * @param dimensions A list or array specifying the size of each desired dimension;
	 * e.g., new NDintMatrix(5, 5) creates a 2D matrix with length and width = 5
	 */
	public NDIntArray(int... dimensions) {
		this.dimensions = dimensions;
		this.asize = new int[dimensions.length];
		
		int size = dimensions[0];
		asize[0] = 1;
		for (int i = 1; i < dimensions.length; i++) {
			size *= dimensions[i];
			asize[i] = asize[i-1] * dimensions[i-1];
		}
		m = new int[size];
	}
	
	/**
	 * Private constructor for cloning.
	 * @param dimensions
	 * @param asize
	 * @param m
	 */
	private NDIntArray(int[] dimensions, int[] asize, int[] m){
		this.dimensions = dimensions;
		this.asize = asize;
		this.m = m;
	}
	
	/**
	 * Sets all values in the array to 0. 
	 */
	public void clear() {
		for (int i = 0; i < m.length; i++) {
			m[i] = 0;
		}
	}
	
	/**
	 * Compares the dimensions of this Array to another; returns true if they match, false otherwise.
	 * @param other The other array to compare against.
	 * @return
	 */
	public boolean dimensionsMatch(NDIntArray other) {
		if (this.dimensions.length != other.dimensions.length)	return false;
		for (int i = 0; i < dimensions.length; i++) {
			if (this.dimensions[i] != other.dimensions[i]) return false;
		}
		return true;
	}
	
	/**
	 * Retrieves the int at the point specified by the coordinates;
	 * the number of coordinates must match the number of dimensions, or an
	 * exception will be thrown.
	 * E.g., get(3, 4, 5) retrieves the point of those coordinates in a 3D matrix. 
	 * @param coords The list or array of coordinates for the desired point.
	 * @return The value at the specified point in the matrix.
	 */
	public int get(int... coords) {
		int idx = 0;
		for (int d = dimensions.length-1; d >= 0; d--) {
			idx += asize[d] * coords[d];
		}
		return m[idx];
	}
	
    public float getFromRawCoordinate(int coord) {
    	return m[coord];
    }
	
	/**
	 * Sets the int value at the point specified by the coordinates;
	 * the number of coordinates must match the number of dimensions, or an
	 * exception will be thrown.
	 * E.g., get(5, 3, 4, 5) sets the point (3,4,5) in a 3D matrix to a value of 5.
	 * @param f The value to set. 
	 * @param coords The list or array of coordinates for the desired point.
	 */
	public void set(int f, int... coords) {
		int idx = 0;
		for (int d = dimensions.length-1; d >= 0; d--) {
			idx += asize[d] * coords[d];
		}
		
		m[idx] = f;
	}
	
	/**
	 * @return The number of dimensions this array has.
	 */
	public int getNumDimensions() {return dimensions.length;}
	
	/**
	 * @param dimension The dimension for which you wish to retrieve the size.
	 * @return The size of the specified dimension.
	 */
	public int getSizeOfDimension(int dimension) {return dimensions[dimension];}
	
	/**
	 * @return An int array describing the dimensions of this Array.
	 */
	public int[] getDimensions() {return dimensions;}
	
	/**
	 * Convenience method for returning the dimensions as a string in the format of #x#x#, 
	 * e.g., 
	 * 5x5x1
	 * @return The string representing the dimensions.
	 */
	public String getDimensionString() {
		StringBuilder sb = new StringBuilder();
		for (int i = 0; i < dimensions.length - 1; i++) {
			sb.append(dimensions[i]);
			sb.append("x");
		}
		sb.append(dimensions[dimensions.length-1]);
		
		return sb.toString();
	}
	
	/**
	 * @return The total size of the array.
	 */
	public int size() {return m.length;}
	
	/**
	 * Returns an iterator for the array;
	 * if you are stepping through the entire array, using the iterator will
	 * be faster than iterating using get().
	 * @return An array iterator.
	 */
	public MatrixIterator iterator() {return new MatrixIterator();}
	
	/**
	 * Retrives the coordinates of the element in this Array with the highest value.
	 * @return Coordinates of max value element.
	 */
    public int[] getCoordinatesOfMaxValue() {
        int[] coords = null;
        float best = -1;
        
        for (MatrixIterator it = this.iterator(); it.hasNext(); it.next()) {
        	if (null == coords || it.get() > best) {
        		best = it.get();
        		coords = it.getCurrentCoordinates();
        	}
        }
        return coords;
    }
	
    /**
     * Creates a clone of this Array.
     */
    public NDIntArray clone() {
    	int[] odimensions = new int[dimensions.length];
    	System.arraycopy(dimensions, 0, odimensions, 0, dimensions.length);
    	
    	int[] oasize = new int[asize.length];
    	System.arraycopy(asize, 0, oasize, 0, asize.length);
    	
    	int[] om = new int[m.length]; 
    	System.arraycopy(m, 0, om, 0, m.length);

    	NDIntArray oarray = new NDIntArray(odimensions, oasize, om);
    	return oarray;
    }
    
	/**
	 * Builds a string representation of this array.
	 */
	public String toString() {
		StringBuilder sb = new StringBuilder();
		if (dimensions.length == 1) {
			for (int i = 0; i < m.length; i++) sb.append(m[i] + " ");
		} else if (dimensions.length == 2) {
			int idx = 0; 
			while (idx < m.length) {
				sb.append(m[idx++] + " ");
				if (idx % dimensions[0] == 0) sb.append("\n"); 
					
			}
		} else {
			// On a 2d screen, this is about the best we can do:
			int idx = 0; 
			while (idx < m.length) {
				sb.append(m[idx++] + " ");
				if (idx % dimensions[0] == 0) sb.append("\n"); 
				if (idx % (dimensions[0] * dimensions[1]) == 0) sb.append("\n");
			}
		}
		return sb.toString();
	}

	/**
	 * FOR TESTING ONLY
	 * @param args
	 */
	public static void main(String[] args) {
		/*
		int xs = 2
		, ys = 4
		, zs = 5
		, ws = 2
		, ts = 2
		;
		int[] a = new int[] {xs, ys, zs, ws, ts};
		NDIntArray fm = new NDIntArray(a);
		for (int t = 0; t < ts; t++) {
			for (int w = 0; w < ws; w++) {
				for (int z = 0; z < zs; z++) {
					for (int y = 0; y < ys; y++) {
						for (int x = 0; x < xs; x++) {
							fm.set((int)(x+y+z+w+t), x, y, z, w, t);
						}
						
					}
				}
			}
		}
		for (int i = 0; i < fm.m.length; i++) System.out.print(fm.m[i] + " ");
				
		System.out.println("\nend raw.");
		
		System.out.println("\n" + fm);
		
		System.out.println("numDim:" + fm.getNumDimensions());
		System.out.println("size:" + fm.size());
		
		for (int i = 0; i < fm.getNumDimensions(); i++) {
			System.out.println("\nDim:" + fm.getSizeOfDimension(i));
		}
		
		System.out.println("Dump:");
		int r;
		for (int t = 0; t < ts; t++) {
			for (int w = 0; w < ws; w++) {
				for (int z = 0; z < zs; z++) {
					for (int y = 0; y < ys; y++) {
						for (int x = 0; x < xs; x++) {
							r = fm.get( x, y, z, w, t);
							System.out.print(r + " ");
						}
						System.out.println();
					}
					System.out.println();
				}
			}
		}
		
		System.out.println("Dump2:");
		MatrixIterator it = fm.iterator();
		while (it.hasNext()) {
			r = it.next();
			System.out.print(r + " ");
		}
		*/
	}
	
	/**
	 * A simple iterator for the array;
	 * if you are stepping through the entire array, using the iterator will
	 * be faster than iterating using get().
	 *
	 */
	public class MatrixIterator implements Iterator {
		private int idx;
		
		private MatrixIterator() {idx = 0;}
		
		@Override
		public boolean hasNext() {return idx < m.length;}

		@Override
		public Integer next() {return m[idx++];}

		@Override
		public void remove() {/* no-op*/}	
		
		public int[] getCurrentCoordinates() {
			int[] c = new int[dimensions.length];
			int localidx = idx;
			for (int dim = dimensions.length-1; dim >=0 ; dim--) {
				c[dim] = localidx / asize[dim];
				localidx -= asize[dim] * c[dim] ;
			}
			return c;
		}
		
		public int getRawCoordinate() {return idx;}
		
		public void set(int i) {m[idx] = i;}
		public int get() {return m[idx];}
		public void reset() {idx = 0;}
	}
}

